iT邦幫忙

2026 iThome 鐵人賽

DAY 21
0
Claude AI

AI 時代下最值得投資的 UI 自動化:30 天用 Claude Code 學會寫 Playwright系列 第 21

Day 21: Page Object Model:請 Claude Code 幫你重構

  • 分享至 

  • xImage
  •  

前面打完,你手上可能有二、三十個測試了。恭喜,同時也先打個預防針:第一次大改版來襲時,你會發現同一個定位散落在十幾個檔案裡。可維護性篇的開場,就從解這個痛開始——Page Object Model,簡稱 POM。

這篇會用 Playwright 官方的 TodoMVC 示範站當素材,從「重構前長怎樣」一路做到「重構後長怎樣」,中間所有程式碼都由 Claude Code 產出。你可以邊讀邊做,全程大約 40 分鐘。

一、痛點:改一顆按鈕,動十四個地方

https://ithelp.ithome.com.tw/upload/images/20260820/20161809KHTr8wcpne.png
圖 1:同一個定位散落各處 vs. 集中在頁面物件

假設登入按鈕改版了,定位要換。如果每個測試都自己寫了一份登入操作,你得在十幾個檔案裡找出全部十四處、一一修改,漏一處就紅一片。

為什麼偏偏是登入?因為它是幾乎每個測試都要先做一次的前置動作。它被複製的次數,等於你的測試檔數量。搜尋、加入購物車、送出表單,這些高頻操作都有同樣的問題。

這不是假設。這是所有自動化團隊在第一次大改版時的集體記憶,也是很多自動化專案的死因:改不動,乾脆放著爛。

二、解法:把頁面知識集中到一個地方
https://ithelp.ithome.com.tw/upload/images/20260820/20161809rsGGaHYNBj.png
圖 2:POM 的兩層分工——測試層問「要驗什麼」,頁面物件層答「怎麼做」

POM 的想法很樸素。每個頁面建一個「頁面物件」:這個頁面有哪些元素、怎麼定位、常見操作(登入、搜尋、加入購物車)怎麼做,全部只寫在這裡。

測試不再自己碰定位,改成呼叫頁面物件提供的操作。兩層各自回答一個問題:

• 頁面物件層回答「怎麼做」:輸入框在哪、要按 Enter 還是點按鈕、送出後要等哪個元素出現。
• 測試層回答「要驗什麼」:走完這段流程之後,畫面應該變成什麼樣子。

畫面改版時,只需要修頁面物件那一處。測試層一行都不用動。

還有個常被低估的附帶好處:測試變得像測試案例了。不寫程式的同事打開測試檔,看到的是「新增待辦(買牛奶)、驗證清單有(買牛奶)」這種句子,看得懂、審得動。測試的可讀性,決定了誰能參與維護。

三、看實際的樣子:TodoMVC 的重構前後

素材用 Playwright 官方示範站 https://demo.playwright.dev/todomvc,不用架環境、不用帳號,打開就能練。

重構前:定位寫在測試裡

// tests/todo-add.spec.ts(重構前)
import { test, expect } from '@playwright/test';
 
test('新增待辦事項會出現在清單', async ({ page }) => {
  await page.goto('https://demo.playwright.dev/todomvc');
  await page.getByPlaceholder('What needs to be done?').fill('買牛奶');
  await page.getByPlaceholder('What needs to be done?').press('Enter');
  await expect(page.getByTestId('todo-item')).toHaveText(['買牛奶']);
});
// tests/todo-complete.spec.ts(重構前)
import { test, expect } from '@playwright/test';
 
test('完成待辦事項後會標記為 completed', async ({ page }) => {
  await page.goto('https://demo.playwright.dev/todomvc');
  await page.getByPlaceholder('What needs to be done?').fill('買牛奶');
  await page.getByPlaceholder('What needs to be done?').press('Enter');
  await page.getByTestId('todo-item').getByRole('checkbox').check();
  await expect(page.getByTestId('todo-item')).toHaveClass(/completed/);
});

兩個檔案,同一段「開頁面 + 新增一筆待辦」寫了兩次。如果哪天 TodoMVC 把輸入框的提示文字從 What needs to be done? 換成別的,這兩處都要改。三十個測試,就是三十處。

重構後:pages/TodoPage.ts

// pages/TodoPage.ts
import { type Page, type Locator } from '@playwright/test';
 
export class TodoPage {
  readonly page: Page;
  readonly newTodoInput: Locator;   // 新增待辦的輸入框
  readonly todoItems: Locator;      // 清單上的每一筆待辦
  readonly counter: Locator;        // 左下角「N items left」
 
  constructor(page: Page) {
    this.page = page;
    this.newTodoInput = page.getByPlaceholder('What needs to be done?');
    this.todoItems = page.getByTestId('todo-item');
    this.counter = page.getByTestId('todo-count');
  }
 
  async goto() {
    await this.page.goto('https://demo.playwright.dev/todomvc');
  }
 
  async addTodo(text: string) {
    await this.newTodoInput.fill(text);
    await this.newTodoInput.press('Enter');
  }
 
  async completeTodo(text: string) {
    await this.itemWithText(text).getByRole('checkbox').check();
  }
 
  async removeTodo(text: string) {
    const item = this.itemWithText(text);
    await item.hover();
    await item.getByRole('button', { name: 'Delete' }).click();
  }
 
  async filterBy(name: 'All' | 'Active' | 'Completed') {
    await this.page.getByRole('link', { name }).click();
  }
 
  // 給測試層做斷言用:回傳 locator,不在這裡下 expect
  itemWithText(text: string): Locator {
    return this.todoItems.filter({ hasText: text });
  }
}

重構後:測試檔只剩流程和斷言

// tests/todo-add.spec.ts(重構後)
import { test, expect } from '@playwright/test';
import { TodoPage } from '../pages/TodoPage';
 
test('新增待辦事項會出現在清單', async ({ page }) => {
  const todoPage = new TodoPage(page);
  await todoPage.goto();
 
  await todoPage.addTodo('買牛奶');
 
  await expect(todoPage.todoItems).toHaveText(['買牛奶']);
});
 
test('完成待辦事項後會標記為 completed', async ({ page }) => {
  const todoPage = new TodoPage(page);
  await todoPage.goto();
 
  await todoPage.addTodo('買牛奶');
  await todoPage.completeTodo('買牛奶');
 
  await expect(todoPage.itemWithText('買牛奶')).toHaveClass(/completed/);
});

把測試念出聲看看:開待辦頁、新增(買牛奶)、完成(買牛奶)、驗證它被標記為完成。這段話你可以直接拿去跟業務單位對案例,中間沒有一個字在講 CSS。

https://ithelp.ithome.com.tw/upload/images/20260820/20161809WY8AdyBTLV.png
圖 3:重構前後的專案結構——多一個 pages/ 資料夾,定位全部搬進去

四、業界寫 POM 的六條慣例

POM 好寫,但也很容易寫歪。下面這六條是各家團隊踩出來的共識,也是你檢查 AI 產出時可以直接拿來對照的清單。
https://ithelp.ithome.com.tw/upload/images/20260820/201618097DpEcnJTyJ.png

第三條有個常見例外:頁面物件可以提供 isLoaded() 或 expectLoaded() 這種「頁面自我檢查」的方法,用來確認頁面已經進到可操作狀態。這跟商業規則的驗證是兩回事,前者是前置條件,後者才是測試的重點,別混在一起。

五、進階一步:用 fixture 把頁面物件送進測試

上面每個測試都要 new 一次、goto 一次,寫多了還是重複。Playwright 的 fixture 可以把這段前置作業收起來,這也是目前社群最常見的 POM 寫法。

// fixtures.ts
import { test as base } from '@playwright/test';
import { TodoPage } from './pages/TodoPage';
 
export const test = base.extend<{ todoPage: TodoPage }>({
  todoPage: async ({ page }, use) => {
    const todoPage = new TodoPage(page);
    await todoPage.goto();      // 每個測試開始前自動開好頁面
    await use(todoPage);        // 把物件交給測試使用
  },
});
 
export { expect } from '@playwright/test';
// tests/todo-add.spec.ts(改用 fixture)
import { test, expect } from '../fixtures';
 
test('新增待辦事項會出現在清單', async ({ todoPage }) => {
  await todoPage.addTodo('買牛奶');
  await expect(todoPage.todoItems).toHaveText(['買牛奶']);
});

測試只剩兩行:做什麼、驗什麼。注意 import 的來源換成了自己的 fixtures,這是唯一要記得的地方。

六、重頭戲:用一句指令重構

這一節的前提是:你手上已經有一批能跑的測試,現在要把它們改成 POM。傳統上這是苦工,也是勸退點;現在它是 Claude Code 最擅長的任務之一。

動手前先確認三件事

• 測試現在是全綠的。有紅燈先修完再重構,否則重構後看到紅燈,你分不出是誰弄壞的。
• 已經 git commit 過,有一個可以退回去的點。AI 一次會動很多檔案,退路要先留好。
• 測試不會飄。同樣的程式碼連跑十次都要是同樣結果,這是下一節「前後對照」能成立的基礎。

如果你的測試本來就會飄(同樣的程式碼,跑十次有一兩次紅),先把飄的修掉再重構。否則重構後出現紅燈,你分不出是重構弄壞的,還是它本來就會這樣。這件事沒有捷徑,跳過它,後面那道閘門就形同虛設。

重構指令
請 Claude Code 這樣做:

請把 tests/ 底下所有測試重構成 Page Object Model:
1) 為出現過的每個頁面建立頁面物件,放在 pages/ 資料夾,檔名用 XxxPage.ts。
2) 定位一律宣告在建構子的 readonly locator;方法用業務語言命名,例如 addTodo、completeTodo,不要用 clickButton 這種名字。
3) 把定位和常用操作移進頁面物件,測試檔只留業務流程和斷言。斷言不要搬進頁面物件。
4) 重構過程不可改變任何測試的行為——重構前先全部跑一遍記錄結果,重構後再跑一遍,兩次結果必須一致。
5) 完成後給我摘要:建了哪些頁面物件、每個測試檔改了什麼,以及重構前後的測試結果對照。
第 4 點是這段指令的靈魂,值得單獨講。
七、重構的鐵律:行為不變,測試為證

https://ithelp.ithome.com.tw/upload/images/20260820/20161809YwWUruzxkA.png
圖 4:重構前後的執行結果比對,是判斷「重構成立」的唯一憑據

重構的定義是「改善結構、不改變行為」。怎麼確認行為沒變?跑測試。重構前全綠,重構後也要全綠,而且是同一批測試在綠。

這個前後對照的紀律,交給 AI 執行時更加重要。它動的檔案又多又快,沒有這道閘門,你分不出「重構」和「順手改壞」。這是用測試守護測試自己,而你,是要求出示證據的人。
實際操作上,把結果存成檔案比用眼睛記可靠:

# 重構前
npx playwright test --reporter=list > before.txt
 
# ...請 Claude Code 重構...
 
# 重構後
npx playwright test --reporter=list > after.txt
diff before.txt after.txt      # 除了執行時間,不該有其他差異

八、怎麼審查 AI 的重構結果

不用逐行讀程式。抽查下面五件事,就有八成把握。
https://ithelp.ithome.com.tw/upload/images/20260820/20161809p5T76IFcjr.png

如果 AI 在重構過程中「順手」把某個斷言改寬鬆了(例如 toHaveText 改成 toContainText),那不是重構,是把測試改得比較容易過。這種變更要單獨拿出來討論,不能混在重構裡。

九、什麼時候不需要 POM

https://ithelp.ithome.com.tw/upload/images/20260820/20161809vVU8KVtV8b.png
圖 5:POM 的價值,跟測試數量與畫面變動頻率成正比

誠實說:測試少於十個、或都是一次性的探索腳本,POM 是過度設計。它的價值跟測試數量與變動頻率成正比。
導入時機的訊號很具體:當你第二次因為同一個畫面改動而修改多個測試檔,就是時候了。也可以從中間做法起步——先把最常重複的那段(通常是登入)抽成一個物件,其他維持原狀,等重複再出現時再抽下一個。

十、今天的練習

重構要有東西可以重構,所以先確認你站在哪個起點:

• 手上已經有 TodoMVC 的測試(實戰篇一路做下來就會有)——跳過練習 1,直接從練習 2 開始。
• 手上沒有,或想用一份乾淨的素材練——先做練習 1 產出素材。

三個練習加起來約 40 分鐘(含練習 1)。第四個回到你們自己的系統。

練習 1(沒有現成測試才做):先做出「重構前」的素材(約 15 分鐘)

開一個空資料夾,讓 Claude Code 幫你建立三個刻意不用 POM 的測試。
請 Claude Code 這樣做:

請在這個資料夾建立 Playwright 專案,並寫三個測試檔,測試對象是 https://demo.playwright.dev/todomvc:
1) todo-add.spec.ts:新增一筆待辦,驗證它出現在清單。
2) todo-complete.spec.ts:新增後標記完成,驗證它被標記為 completed。
3) todo-filter.spec.ts:新增兩筆、完成其中一筆,分別切到 Active 和 Completed 頁籤,驗證清單內容。

這一版故意不要用 Page Object Model,定位直接寫在每個測試裡。寫完幫我跑一次,把執行結果貼給我。
跑完先看一眼三個檔案:那段「開頁面、新增一筆待辦」是不是重複了三次?這就是待會要處理的東西。

練習 2:請 Claude Code 重構(約 10 分鐘)

先做第六節那三個確認:測試全綠、已經 commit、不會飄。三項都過了,再用第六節那段指令,把 tests/ 重構成一個 TodoPage 物件。記得要求重構前後的結果對照,不要省略。

如果你想多練一步,重構完之後再下一句:「請把 TodoPage 改成用 fixture 注入,測試檔不要再自己 new。」對照一下前後兩版,你會很有感覺。

練習 3:驗收(約 10 分鐘)

拿第八節的五個抽查點逐項對過去。特別做這兩件事:

• 全專案搜尋 getByPlaceholder,確認它只出現在 pages/ 底下。
• 打開一個測試檔,念給自己聽,是不是變成了「新增待辦(買牛奶)、驗證清單有(買牛奶)」這種業務語言。

有任何一項不過關,把問題貼回去請 Claude Code 修,不要自己默默改。你要練的是「能講清楚哪裡不對」,這個能力比會改程式重要。

練習 4:回你們自己的系統

同一招套用在自己的測試套件上。有一個細節請堅持:頁面物件的操作命名,用你們團隊的行話——進件、核價、派工、結案——不要用 submitForm 這種泛用詞。測試讀起來越像你們平常在會議室講的話,能參與審查的人就越多。

重構完之後,把 pages/ 這個資料夾放進 code review 的必看清單。往後幾年,它會是整個測試專案裡最常被改動、也最值得被看的地方。


上一篇
Day20: 多瀏覽器與行動裝置模擬
下一篇
Day22: Fixtures 與測試資料管理
系列文
AI 時代下最值得投資的 UI 自動化:30 天用 Claude Code 學會寫 Playwright22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言